﻿using System;
using System.Security.Cryptography;
using System.Text;

namespace Loki.Utils
{
    public static class HashHelper
    {
        public static string ComputeHash(
            string P_Text,
            HashAlgorithmType P_Algorithm)
        {
            return ComputeHash(P_Text, P_Algorithm, null);
        }

        /// <summary>
        /// Generates a hash for the given plain text value and returns a
        /// base64-encoded result. Before the hash is computed, a random salt
        /// is generated and appended to the plain text. This salt is stored at
        /// the end of the hash value, so it can be used later for hash
        /// verification.
        /// </summary>
        /// <param name="P_Text">
        /// Plaintext value to be hashed. The function does not check whether
        /// this parameter is null.
        /// </param>
        /// <param name="P_Algorithm">
        /// Name of the hash algorithm. Allowed values are: "MD5", "SHA1",
        /// "SHA256", "SHA384", and "SHA512" (if any other value is specified
        /// MD5 hashing algorithm will be used). This value is case-insensitive.
        /// </param>
        /// <param name="P_Salt">
        /// Salt bytes. This parameter can be null, in which case a random salt
        /// value will be generated.
        /// </param>
        /// <returns>
        /// Hash value formatted as a base64-encoded string.
        /// </returns>
        public static string ComputeHash(
                                         string P_Text,
                                         HashAlgorithmType P_Algorithm,
                                         byte[] P_Salt)
        {
            // If salt is not specified, generate it on the fly.
            if (P_Salt == null)
            {
                // Define min and max salt sizes.
                int L_MinSaltSize = 4;
                int L_MaxSaltSize = 8;

                // Generate a random number for the size of the salt.
                Random L_Random = new Random();
                int L_SaltSize = L_Random.Next(L_MinSaltSize, L_MaxSaltSize);

                // Allocate a byte array, which will hold the salt.
                P_Salt = new byte[L_SaltSize];

                // Initialize a random number generator.
                using (RNGCryptoServiceProvider L_RNG = new RNGCryptoServiceProvider())
                {
                    // Fill the salt with cryptographically strong byte values.
                    L_RNG.GetNonZeroBytes(P_Salt);
                }
            }

            // Convert plain text into a byte array.
            byte[] L_TextBytes = Encoding.UTF8.GetBytes(P_Text);

            // Allocate array, which will hold plain text and salt.
            byte[] L_TextWithSaltBytes =
                    new byte[L_TextBytes.Length + P_Salt.Length];

            // Copy plain text bytes into resulting array.
            for (int i = 0; i < L_TextBytes.Length; i++)
            {
                L_TextWithSaltBytes[i] = L_TextBytes[i];
            }

            // Append salt bytes to the resulting array.
            for (int i = 0; i < P_Salt.Length; i++)
            {
                L_TextWithSaltBytes[L_TextBytes.Length + i] = P_Salt[i];
            }

            // Because we support multiple hashing algorithms, we must define
            // hash object as a common (abstract) base class. We will specify the
            // actual hashing algorithm class later during object creation.
            HashAlgorithm L_Hash;

            // Initialize appropriate hashing algorithm class.
            switch (P_Algorithm)
            {
                case HashAlgorithmType.SHA1:
                    L_Hash = new SHA1Managed();
                    break;

                case HashAlgorithmType.SHA256:
                    L_Hash = new SHA256Managed();
                    break;

                case HashAlgorithmType.SHA384:
                    L_Hash = new SHA384Managed();
                    break;

                case HashAlgorithmType.SHA512:
                    L_Hash = new SHA512Managed();
                    break;
                case HashAlgorithmType.MD5:
                default:
                    L_Hash = new MD5CryptoServiceProvider();
                    break;
            }

            try
            {
                // Compute hash value of our plain text with appended salt.
                byte[] L_HashBytes = L_Hash.ComputeHash(L_TextWithSaltBytes);

                // Create array which will hold hash and original salt bytes.
                byte[] L_HashWithSaltBytes = new byte[L_HashBytes.Length +
                                                    P_Salt.Length];

                // Copy hash bytes into resulting array.
                for (int i = 0; i < L_HashBytes.Length; i++)
                {
                    L_HashWithSaltBytes[i] = L_HashBytes[i];
                }

                // Append salt bytes to the result.
                for (int i = 0; i < P_Salt.Length; i++)
                {
                    L_HashWithSaltBytes[L_HashBytes.Length + i] = P_Salt[i];
                }

                // Convert result into a base64-encoded string.
                string L_HashValue = Convert.ToBase64String(L_HashWithSaltBytes);

                // Return the result.
                return L_HashValue;
            }
            finally
            {
                L_Hash.Dispose();
            }
        }

        /// <summary>
        /// Compares a hash of the specified plain text value to a given hash
        /// value. Plain text is hashed with the same salt value as the original
        /// hash.
        /// </summary>
        /// <param name="P_Text">
        /// Plain text to be verified against the specified hash. The function
        /// does not check whether this parameter is null.
        /// </param>
        /// <param name="P_Algorithm">
        /// Name of the hash algorithm. Allowed values are: "MD5", "SHA1", 
        /// "SHA256", "SHA384", and "SHA512" (if any other value is specified,
        /// MD5 hashing algorithm will be used). This value is case-insensitive.
        /// </param>
        /// <param name="P_Hash">
        /// Base64-encoded hash value produced by ComputeHash function. This value
        /// includes the original salt appended to it.
        /// </param>
        /// <returns>
        /// If computed hash mathes the specified hash the function the return
        /// value is true; otherwise, the function returns false.
        /// </returns>
        public static bool VerifyHash(
                                      string P_Text,
                                      HashAlgorithmType P_Algorithm,
                                      string P_Hash)
        {
            // Convert base64-encoded hash value into a byte array.
            byte[] L_HashWithSaltBytes = Convert.FromBase64String(P_Hash);

            // We must know size of hash (without salt).
            int L_HashSizeInBits, L_HashSizeInBytes;

            // Size of hash is based on the specified algorithm.
            switch (P_Algorithm)
            {
                case HashAlgorithmType.SHA1:
                    L_HashSizeInBits = 160;
                    break;

                case HashAlgorithmType.SHA256:
                    L_HashSizeInBits = 256;
                    break;

                case HashAlgorithmType.SHA384:
                    L_HashSizeInBits = 384;
                    break;

                case HashAlgorithmType.SHA512:
                    L_HashSizeInBits = 512;
                    break;
                case HashAlgorithmType.MD5:
                default: // Must be MD5
                    L_HashSizeInBits = 128;
                    break;
            }

            // Convert size of hash from bits to bytes.
            L_HashSizeInBytes = L_HashSizeInBits / 8;

            // Make sure that the specified hash value is long enough.
            if (L_HashWithSaltBytes.Length < L_HashSizeInBytes)
            {
                return false;
            }

            // Allocate array to hold original salt bytes retrieved from hash.
            byte[] L_SaltBytes = new byte[L_HashWithSaltBytes.Length -
                                        L_HashSizeInBytes];

            // Copy salt from the end of the hash to the new array.
            for (int i = 0; i < L_SaltBytes.Length; i++)
            {
                L_SaltBytes[i] = L_HashWithSaltBytes[L_HashSizeInBytes + i];
            }

            // Compute a new hash string.
            string L_ExpectedHashString =
                        ComputeHash(P_Text, P_Algorithm, L_SaltBytes);

            // If the computed hash matches the specified hash,
            // the plain text value must be correct.
            return P_Hash == L_ExpectedHashString;
        }
    }
}
